msg_tool\scripts\circus\archive/
crm.rs

1//! Circus Image Archive File (.crm)
2use crate::ext::io::*;
3use crate::scripts::base::*;
4use crate::types::*;
5use anyhow::Result;
6use std::collections::BTreeMap;
7use std::io::{Read, Seek, SeekFrom};
8use std::sync::{Arc, Mutex};
9
10#[derive(Debug)]
11/// Circus CRM Archive Builder
12pub struct CrmArchiveBuilder {}
13
14impl CrmArchiveBuilder {
15    /// Creates a new instance of `CrmArchiveBuilder`.
16    pub fn new() -> Self {
17        Self {}
18    }
19}
20
21impl ScriptBuilder for CrmArchiveBuilder {
22    fn default_encoding(&self) -> Encoding {
23        Encoding::Cp932
24    }
25
26    fn default_archive_encoding(&self) -> Option<Encoding> {
27        Some(Encoding::Cp932)
28    }
29
30    fn build_script(
31        &self,
32        data: Vec<u8>,
33        _filename: &str,
34        _encoding: Encoding,
35        archive_encoding: Encoding,
36        config: &ExtraConfig,
37        _archive: Option<&Box<dyn Script>>,
38    ) -> Result<Box<dyn Script + Send + Sync>> {
39        Ok(Box::new(CrmArchive::new(
40            MemReader::new(data),
41            archive_encoding,
42            config,
43        )?))
44    }
45
46    fn build_script_from_file(
47        &self,
48        filename: &str,
49        _encoding: Encoding,
50        archive_encoding: Encoding,
51        config: &ExtraConfig,
52        _archive: Option<&Box<dyn Script>>,
53    ) -> Result<Box<dyn Script + Send + Sync>> {
54        if filename == "-" {
55            let data = crate::utils::files::read_file(filename)?;
56            Ok(Box::new(CrmArchive::new(
57                MemReader::new(data),
58                archive_encoding,
59                config,
60            )?))
61        } else {
62            let f = std::fs::File::open(filename)?;
63            let reader = std::io::BufReader::new(f);
64            Ok(Box::new(CrmArchive::new(reader, archive_encoding, config)?))
65        }
66    }
67
68    fn build_script_from_reader<'a>(
69        &self,
70        reader: Box<dyn ReadSeek + Send + Sync + 'a>,
71        _filename: &str,
72        _encoding: Encoding,
73        archive_encoding: Encoding,
74        config: &ExtraConfig,
75        _archive: Option<&Box<dyn Script>>,
76    ) -> Result<Box<dyn Script + Send + Sync + 'a>> {
77        Ok(Box::new(CrmArchive::new(reader, archive_encoding, config)?))
78    }
79
80    fn extensions(&self) -> &'static [&'static str] {
81        &["crm"]
82    }
83
84    fn script_type(&self) -> &'static ScriptType {
85        &ScriptType::CircusCrm
86    }
87
88    fn is_archive(&self) -> bool {
89        true
90    }
91
92    fn is_this_format(&self, _filename: &str, buf: &[u8], buf_len: usize) -> Option<u8> {
93        if buf_len >= 4 && buf.starts_with(b"CRXB") {
94            return Some(10);
95        }
96        None
97    }
98}
99
100#[derive(Debug, Clone)]
101struct CrmFileHeader {
102    offset: u32,
103    size: u32,
104    name: String,
105}
106
107#[derive(Debug)]
108struct Entry<T: Read + Seek + std::fmt::Debug> {
109    header: CrmFileHeader,
110    reader: Arc<Mutex<T>>,
111    pos: usize,
112    script_type: Option<ScriptType>,
113}
114
115impl<T: Read + Seek + std::fmt::Debug + Send + Sync> ArchiveContent for Entry<T> {
116    fn name(&self) -> &str {
117        &self.header.name
118    }
119
120    fn size(&self) -> Option<u64> {
121        Some(self.header.size as u64)
122    }
123
124    fn script_type(&self) -> Option<&ScriptType> {
125        self.script_type.as_ref()
126    }
127
128    fn to_data<'a>(&'a mut self) -> Result<Box<dyn ReadSeek + Send + Sync + 'a>> {
129        Ok(Box::new(self))
130    }
131}
132
133impl<T: Read + Seek + std::fmt::Debug> Read for Entry<T> {
134    fn read(&mut self, buf: &mut [u8]) -> std::io::Result<usize> {
135        let mut reader = self.reader.lock().map_err(|e| {
136            std::io::Error::new(
137                std::io::ErrorKind::Other,
138                format!("Failed to lock mutex: {}", e),
139            )
140        })?;
141        reader.seek(SeekFrom::Start(self.header.offset as u64 + self.pos as u64))?;
142        let bytes_read = buf.len().min(self.header.size as usize - self.pos);
143        if bytes_read == 0 {
144            return Ok(0);
145        }
146        let bytes_read = reader.read(&mut buf[..bytes_read])?;
147        self.pos += bytes_read;
148        Ok(bytes_read)
149    }
150}
151
152impl<T: Read + Seek + std::fmt::Debug> Seek for Entry<T> {
153    fn seek(&mut self, pos: SeekFrom) -> std::io::Result<u64> {
154        let new_pos = match pos {
155            SeekFrom::Start(offset) => offset as usize,
156            SeekFrom::End(offset) => {
157                if offset < 0 {
158                    if (-offset) as usize > self.header.size as usize {
159                        return Err(std::io::Error::new(
160                            std::io::ErrorKind::InvalidInput,
161                            "Seek from end exceeds file length",
162                        ));
163                    }
164                    self.header.size as usize - (-offset) as usize
165                } else {
166                    self.header.size as usize + offset as usize
167                }
168            }
169            SeekFrom::Current(offset) => {
170                if offset < 0 {
171                    if (-offset) as usize > self.pos {
172                        return Err(std::io::Error::new(
173                            std::io::ErrorKind::InvalidInput,
174                            "Seek from current exceeds current position",
175                        ));
176                    }
177                    self.pos.saturating_sub((-offset) as usize)
178                } else {
179                    self.pos + offset as usize
180                }
181            }
182        };
183        self.pos = new_pos;
184        Ok(self.pos as u64)
185    }
186
187    fn stream_position(&mut self) -> std::io::Result<u64> {
188        Ok(self.pos as u64)
189    }
190}
191
192#[derive(Debug)]
193/// Circus CRM Archive
194pub struct CrmArchive<'b, T: Read + Seek + std::fmt::Debug + 'b> {
195    reader: Arc<Mutex<T>>,
196    entries: Vec<CrmFileHeader>,
197    _mark: std::marker::PhantomData<&'b ()>,
198}
199
200impl<'b, T: Read + Seek + std::fmt::Debug + 'b> CrmArchive<'b, T> {
201    /// Creates a new `CrmArchive` from a reader.
202    ///
203    /// * `reader` - The reader to read the CRM archive from.
204    /// * `encoding` - The encoding to use for string fields.
205    /// * `config` - Extra configuration options.
206    pub fn new(mut reader: T, encoding: Encoding, _config: &ExtraConfig) -> Result<Self> {
207        let mut magic = [0u8; 4];
208        reader.read_exact(&mut magic)?;
209        if &magic != b"CRXB" {
210            return Err(anyhow::anyhow!("Invalid CRM archive magic: {:?}", magic));
211        }
212        reader.seek_relative(4)?;
213        let count = reader.read_u32()? as usize;
214        reader.seek_relative(4)?;
215        let mut entries = Vec::with_capacity(count);
216        let file_len = reader.stream_length()?;
217        let mut offset_map = BTreeMap::new();
218        for _ in 0..count {
219            let offset = reader.read_u32()?;
220            reader.seek_relative(4)?;
221            let name = reader.read_fstring(0x18, encoding, true)?;
222            offset_map.insert(offset, name);
223        }
224        let mut next_iter = offset_map.keys().skip(1);
225        for (offset, name) in &offset_map {
226            let size = if let Some(next) = next_iter.next() {
227                *next
228            } else {
229                file_len as u32
230            } - offset;
231            entries.push(CrmFileHeader {
232                offset: *offset,
233                size,
234                name: name.clone(),
235            });
236        }
237        Ok(Self {
238            reader: Arc::new(Mutex::new(reader)),
239            entries,
240            _mark: std::marker::PhantomData,
241        })
242    }
243}
244
245impl<'b, T: Read + Seek + std::fmt::Debug + Send + Sync + 'b> Script for CrmArchive<'b, T> {
246    fn default_output_script_type(&self) -> OutputScriptType {
247        OutputScriptType::Json
248    }
249
250    fn default_format_type(&self) -> FormatOptions {
251        FormatOptions::None
252    }
253
254    fn is_archive(&self) -> bool {
255        true
256    }
257
258    fn iter_archive_filename<'a>(
259        &'a self,
260    ) -> Result<Box<dyn Iterator<Item = Result<String>> + 'a>> {
261        Ok(Box::new(self.entries.iter().map(|e| Ok(e.name.clone()))))
262    }
263
264    fn iter_archive_offset<'a>(&'a self) -> Result<Box<dyn Iterator<Item = Result<u64>> + 'a>> {
265        Ok(Box::new(self.entries.iter().map(|e| Ok(e.offset as u64))))
266    }
267
268    fn open_file<'a>(&'a self, index: usize) -> Result<Box<dyn ArchiveContent + Send + Sync + 'a>> {
269        if index >= self.entries.len() {
270            return Err(anyhow::anyhow!(
271                "Index out of bounds: {} (max: {})",
272                index,
273                self.entries.len()
274            ));
275        }
276        let entry = &self.entries[index];
277        let mut entry = Entry {
278            header: entry.clone(),
279            reader: self.reader.clone(),
280            pos: 0,
281            script_type: None,
282        };
283        let mut buf = [0; 32];
284        let readed = match entry.read(&mut buf) {
285            Ok(readed) => readed,
286            Err(e) => {
287                return Err(anyhow::anyhow!(
288                    "Failed to read entry '{}': {}",
289                    entry.header.name,
290                    e
291                ));
292            }
293        };
294        entry.pos = 0;
295        entry.script_type = detect_script_type(&buf, readed, &entry.header.name);
296        Ok(Box::new(entry))
297    }
298}
299
300fn detect_script_type(_buf: &[u8], _buf_len: usize, _filename: &str) -> Option<ScriptType> {
301    #[cfg(feature = "circus-img")]
302    if _buf_len >= 4 && _buf.starts_with(b"CRXG") {
303        return Some(ScriptType::CircusCrx);
304    }
305    #[cfg(feature = "circus-img")]
306    if _buf_len >= 4 && _buf.starts_with(b"CRXD") {
307        return Some(ScriptType::CircusCrxd);
308    }
309    None
310}